/*
 * $Id: LuaJavaAPI.java,v 1.4 2006/12/22 14:06:40 thiago Exp $
 * Copyright (C) 2003-2007 Kepler Project.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package org.keplerproject.luajava;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/**
 * Class that contains functions accessed by lua.
 * 
 * @author Thiago Ponte
 */
public final class LuaJavaAPI {

	private LuaJavaAPI() {
	}

	/**
	 * Java implementation of the metamethod __index
	 * 
	 * @param luaState
	 *            int that indicates the state used
	 * @param obj
	 *            Object to be indexed
	 * @param methodName
	 *            the name of the method
	 * @return number of returned objects
	 */
	public static int objectIndex(int luaState, Object obj, String methodName)
			throws LuaException {
		LuaState L = LuaStateFactory.getExistingState(luaState);

		synchronized (L) {
			int top = L.getTop();

			Object[] objs = new Object[top - 1];

			Class<?> clazz;

			if (obj instanceof Class) {
				clazz = (Class<?>) obj;
			} else {
				clazz = obj.getClass();
			}

			Method[] methods = clazz.getMethods();
			Method method = null;

			// gets method and arguments
			for (int i = 0; i < methods.length; i++) {
				if (!methods[i].getName().equals(methodName))

					continue;

				Class<?>[] parameters = methods[i].getParameterTypes();
				if (parameters.length != top - 1)
					continue;

				boolean okMethod = true;

				for (int j = 0; j < parameters.length; j++) {
					try {
						objs[j] = compareTypes(L, parameters[j], j + 2);
					} catch (Exception e) {
						okMethod = false;
						break;
					}
				}

				if (okMethod) {
					method = methods[i];
					break;
				}

			}

			// If method is null means there isn't one receiving the given
			// arguments
			if (method == null) {
				throw new LuaException("Invalid method call. No such method.");
			}

			Object ret;
			try {
				if (Modifier.isPublic(method.getModifiers())) {
					method.setAccessible(true);
				}

				if (obj instanceof Class) {
					ret = method.invoke(null, objs);
				} else {
					ret = method.invoke(obj, objs);
				}
			} catch (Exception e) {
				throw new LuaException(e);
			}

			// Void function returns null
			if (ret == null) {
				return 0;
			}

			// push result
			L.pushObjectValue(ret);

			return 1;
		}
	}

	/**
	 * Java function to be called when a java Class metamethod __index is
	 * called. This function returns 1 if there is a field with searchName and 2
	 * if there is a method if the searchName
	 * 
	 * @param luaState
	 *            int that represents the state to be used
	 * @param clazz
	 *            class to be indexed
	 * @param searchName
	 *            name of the field or method to be accessed
	 * @return number of returned objects
	 * @throws LuaException
	 */
	public static int classIndex(int luaState, Class<?> clazz, String searchName)
			throws LuaException {
		synchronized (LuaStateFactory.getExistingState(luaState)) {
			int res;

			res = checkField(luaState, clazz, searchName);

			if (res != 0) {
				return 1;
			}

			res = checkMethod(luaState, clazz, searchName);

			if (res != 0) {
				return 2;
			}

			return 0;
		}
	}

	/**
	 * Pushes a new instance of a java Object of the type className
	 * 
	 * @param luaState
	 *            int that represents the state to be used
	 * @param className
	 *            name of the class
	 * @return number of returned objects
	 * @throws LuaException
	 */
	public static int javaNewInstance(int luaState, String className)
			throws LuaException {
		LuaState L = LuaStateFactory.getExistingState(luaState);

		synchronized (L) {
			Class<?> clazz;
			try {
				clazz = Class.forName(className);
			} catch (ClassNotFoundException e) {
				throw new LuaException(e);
			}
			Object ret = getObjInstance(L, clazz);

			L.pushJavaObject(ret);

			return 1;
		}
	}

	/**
	 * javaNew returns a new instance of a given clazz
	 * 
	 * @param luaState
	 *            int that represents the state to be used
	 * @param clazz
	 *            class to be instanciated
	 * @return number of returned objects
	 * @throws LuaException
	 */
	public static int javaNew(int luaState, Class<?> clazz) throws LuaException {
		LuaState L = LuaStateFactory.getExistingState(luaState);

		synchronized (L) {
			Object ret = getObjInstance(L, clazz);

			L.pushJavaObject(ret);

			return 1;
		}
	}

	/**
	 * Calls the static method <code>methodName</code> in class
	 * <code>className</code> that receives a LuaState as first parameter.
	 * 
	 * @param luaState
	 *            int that represents the state to be used
	 * @param className
	 *            name of the class that has the open library method
	 * @param methodName
	 *            method to open library
	 * @return number of returned objects
	 * @throws LuaException
	 */
	public static int javaLoadLib(int luaState, String className,
			String methodName) throws LuaException {
		LuaState L = LuaStateFactory.getExistingState(luaState);

		synchronized (L) {
			Class<?> clazz;
			try {
				clazz = Class.forName(className);
			} catch (ClassNotFoundException e) {
				throw new LuaException(e);
			}

			try {
				Method mt = clazz.getMethod(methodName,
						new Class[] { LuaState.class });
				Object obj = mt.invoke(null, new Object[] { L });

				if (obj != null && obj instanceof Integer) {
					return ((Integer) obj).intValue();
				} else
					return 0;
			} catch (Exception e) {
				throw new LuaException(
						"Error on calling method. Library could not be loaded. "
								+ e.getMessage());
			}
		}
	}

	private static Object getObjInstance(LuaState L, Class<?> clazz)
			throws LuaException {
		synchronized (L) {
			int top = L.getTop();

			Object[] objs = new Object[top - 1];

			Constructor<?>[] constructors = clazz.getConstructors();
			Constructor<?> constructor = null;

			// gets method and arguments
			for (int i = 0; i < constructors.length; i++) {
				Class<?>[] parameters = constructors[i].getParameterTypes();
				if (parameters.length != top - 1)
					continue;

				boolean okConstruc = true;

				for (int j = 0; j < parameters.length; j++) {
					try {
						objs[j] = compareTypes(L, parameters[j], j + 2);
					} catch (Exception e) {
						okConstruc = false;
						break;
					}
				}

				if (okConstruc) {
					constructor = constructors[i];
					break;
				}

			}

			// If method is null means there isn't one receiving the given
			// arguments
			if (constructor == null) {
				throw new LuaException("Invalid method call. No such method.");
			}

			Object ret;
			try {
				ret = constructor.newInstance(objs);
			} catch (Exception e) {
				throw new LuaException(e);
			}

			if (ret == null) {
				throw new LuaException("Couldn't instantiate java Object");
			}

			return ret;
		}
	}

	/**
	 * Checks if there is a field on the obj with the given name
	 * 
	 * @param luaState
	 *            int that represents the state to be used
	 * @param obj
	 *            object to be inspected
	 * @param fieldName
	 *            name of the field to be inpected
	 * @return number of returned objects
	 */
	public static int checkField(int luaState, Object obj, String fieldName)
			throws LuaException {
		LuaState L = LuaStateFactory.getExistingState(luaState);

		synchronized (L) {
			Field field = null;
			Class<?> objClass;

			if (obj == null) {
				return 0;
			}

			if (obj instanceof Class) {
				objClass = (Class<?>) obj;
			} else {
				objClass = obj.getClass();
			}

			try {
				field = objClass.getField(fieldName);
			} catch (Exception e) {
				return 0;
			}

			if (field == null) {
				return 0;
			}

			Object ret = null;
			try {
				ret = field.get(obj);
			} catch (Exception e1) {
				return 0;
			}

			L.pushObjectValue(ret);

			return 1;
		}
	}

	/**
	 * Checks to see if there is a method with the given name.
	 * 
	 * @param luaState
	 *            int that represents the state to be used
	 * @param obj
	 *            object to be inspected
	 * @param methodName
	 *            name of the field to be inpected
	 * @return number of returned objects
	 */
	public static int checkMethod(int luaState, Object obj, String methodName) {
		LuaState L = LuaStateFactory.getExistingState(luaState);

		synchronized (L) {
			Class<?> clazz;

			if (obj instanceof Class) {
				clazz = (Class<?>) obj;
			} else {
				clazz = obj.getClass();
			}

			Method[] methods = clazz.getMethods();

			for (int i = 0; i < methods.length; i++) {
				if (methods[i].getName().equals(methodName))
					return 1;
			}

			return 0;
		}
	}

	/**
	 * Function that creates an object proxy and pushes it into the stack
	 * 
	 * @param luaState
	 *            int that represents the state to be used
	 * @param implem
	 *            interfaces implemented separated by comma (<code>,</code>)
	 * @return number of returned objects
	 * @throws LuaException
	 */
	public static int createProxyObject(int luaState, String implem)
			throws LuaException {
		LuaState L = LuaStateFactory.getExistingState(luaState);

		synchronized (L) {
			try {
				if (!(L.isTable(2)))
					throw new LuaException(
							"Parameter is not a table. Can't create proxy.");

				LuaObject luaObj = L.getLuaObject(2);

				Object proxy = luaObj.createProxy(implem);
				L.pushJavaObject(proxy);
			} catch (Exception e) {
				throw new LuaException(e);
			}

			return 1;
		}
	}

	private static Object compareTypes(LuaState L, Class<?> parameter, int idx)
			throws LuaException {
		boolean okType = true;
		Object obj = null;

		if (L.isBoolean(idx)) {
			if (parameter.isPrimitive()) {
				if (parameter != Boolean.TYPE) {
					okType = false;
				}
			} else if (!parameter.isAssignableFrom(Boolean.class)) {
				okType = false;
			}
			obj = new Boolean(L.toBoolean(idx));
		} else if (L.type(idx) == LuaState.LUA_TSTRING.intValue()) {
			if (!parameter.isAssignableFrom(String.class)) {
				okType = false;
			} else {
				obj = L.toString(idx);
			}
		} else if (L.isFunction(idx)) {
			if (!parameter.isAssignableFrom(LuaObject.class)) {
				okType = false;
			} else {
				obj = L.getLuaObject(idx);
			}
		} else if (L.isTable(idx)) {
			if (!parameter.isAssignableFrom(LuaObject.class)) {
				okType = false;
			} else {
				obj = L.getLuaObject(idx);
			}
		} else if (L.type(idx) == LuaState.LUA_TNUMBER.intValue()) {
			Double db = new Double(L.toNumber(idx));

			obj = LuaState.convertLuaNumber(db, parameter);
			if (obj == null) {
				okType = false;
			}
		} else if (L.isUserdata(idx)) {
			if (L.isObject(idx)) {
				Object userObj = L.getObjectFromUserdata(idx);
				if (!parameter.isAssignableFrom(userObj.getClass())) {
					okType = false;
				} else {
					obj = userObj;
				}
			} else {
				if (!parameter.isAssignableFrom(LuaObject.class)) {
					okType = false;
				} else {
					obj = L.getLuaObject(idx);
				}
			}
		} else if (L.isNil(idx)) {
			obj = null;
		} else {
			throw new LuaException("Invalid Parameters.");
		}

		if (!okType) {
			throw new LuaException("Invalid Parameter.");
		}

		return obj;
	}

}